import 'dart:async';
import 'dart:io';

import 'package:flutter/services.dart';
import 'package:flutter_blue/flutter_blue.dart';

class BlePlugin {
    static const MethodChannel _channel =
    const MethodChannel('ble_plugin');

    static Future<String> get platformVersion async {
        final String version = await _channel.invokeMethod('getPlatformVersion');
        return version;
    }

}

typedef void OnNotified(ActiveDevice device, List<int> values);
typedef void OnStateChanged(BluetoothDevice device, BluetoothDeviceState state, {String? flag});


class BleManager {

    BleManager._privateConstructor();

    static final BleManager _instance = BleManager._privateConstructor();

    factory BleManager(){
        return _instance;
    }

    FlutterBlue flutterBle = FlutterBlue.instance;

    List<BleUuid> supportedUuidList = [];

    Map<String, StreamSubscription<BluetoothDeviceState>> connectingPool = {};

    Map<String, ActiveDevice> activeDevices = {};

    /// 蓝牙状态flag map
    /// identifier -> {state -> flag}
    Map<String, Map<BluetoothDeviceState, String>> btStateFlagMap = {};

    OnNotified ?onNotifiedCallback;
    OnStateChanged ?onStateChangedCallback;

    BluetoothState btState = BluetoothState.unknown;

    String? nameToConnect;

    Future<BluetoothState> getBtState() {
        return flutterBle.state.first;
    }

    ActiveDevice? getActiveDevice(String id) {
        return activeDevices[id];
    }

    void addSupportedBleService(BleUuid uuidInfo) {
        supportedUuidList.add(uuidInfo);
    }

    void addSupportedBleServices(List<BleUuid> uuidList) {
        supportedUuidList.addAll(uuidList);
    }

    Stream<ScanResult> scan({ List<Guid> withServices = const [], List<Guid> withDevices = const [], Duration? timeout, bool interruptLastIfExist = true}) {
        timeout ??= const Duration(seconds: 10);

        StreamController<ScanResult> streamController = StreamController();
        scan0() {
            return flutterBle.scan(scanMode: ScanMode.balanced,
                withServices: withServices,
                withDevices: withDevices,
                timeout: timeout,
                allowDuplicates: false).where((scanResult) {
                return true;
            });
        }

        flutterBle.isScanning.first.then((isScanning) {
            if (isScanning) {
                flutterBle.stopScan().then((none) {
                    streamController.addStream(scan0());
                });
                return;
            }
            streamController.addStream(scan0());
        });

        return streamController.stream;
    }

    Future<dynamic> startScan({Duration? timeout}) {
        return flutterBle.startScan(allowDuplicates: false, timeout: timeout);
    }

    void stopScan() {
        flutterBle.isScanning.first.then((isScanning) {
            if (isScanning) return flutterBle.stopScan();
        });
    }

    Future<void> connect({BluetoothDevice ?device, String ?name, Duration? timeout, String? flag, Duration? scanTimeout}) {
        if (device != null) {
            return _connectByDevice(device, flag: flag, timeout: timeout);
        }

        if (name != null) {
            return _connectByName(name, scanTimeout: scanTimeout, flag: flag);
        }
        throw Exception('An error occurred when connect.[Unknown target device to connect.]');
    }

    Future<void> _connectByDevice(BluetoothDevice device, {Duration? timeout, String? flag}) {
        Future future = device.connect(timeout: timeout, autoConnect: false);
        handleBtStateFlagIfNeed(device.id.id, BluetoothDeviceState.connected, flag);
        // listen ble connection state change event

        StreamSubscription<BluetoothDeviceState> stateChangeStream = device.state.skip(1).listen((state) {
            print("device [" + device.name + "] state changed to " + state.toString());
            switch (state) {
                case BluetoothDeviceState.connected:
                    device.discoverServices().then((services) {
                        // listen ble services discovery event
                        String printInfo = "";
                        for (BluetoothService service in services) {
                            printInfo += service.uuid.toString();
                            printInfo += ", characteristic: ";
                            List<BluetoothCharacteristic> characteristics = service.characteristics;
                            for (BluetoothCharacteristic characteristic in characteristics) {
                                printInfo += characteristic.uuid.toString() + ", ";
                            }
                            printInfo += "\n";
                        }
                        print("services: " + "\n" + printInfo);
                        if (services.isEmpty) {
                            print("device [" + device.name + "] has no services.");
                            device.disconnect();
                            // TODO
                            return;
                        }
                        ActiveDevice ?activeDevice = retrieveActiveDevice(device, services);
                        if (activeDevice == null) {
                            print("can not retrieve active device from [" + device.name + "].");
                            device.disconnect();
                            // TODO
                            return;
                        }
                        Future.delayed(Duration(milliseconds: 50), () {
                            if (Platform.isAndroid) {
                                device.requestMtu(256);
                            }
                        });
                        activeDevices[device.id.toString()] = activeDevice;
                        activeDevice.onDataNotifiedSubscription = activeDevice.notifyCharacteristic.value.listen((values) {
//                                print("Received data: " + values.toString());
                            if (onNotifiedCallback != null) onNotifiedCallback!(activeDevice, values);
                        });
                        if (onStateChangedCallback != null) onStateChangedCallback!(
                            device, BluetoothDeviceState.connected, flag: getBtStateFlag(device.id.id, BluetoothDeviceState.connected));
                        device.mtu.listen((event) {
                            print("on mtu changed, $event");
                        });
                    });
                    break;
                case BluetoothDeviceState.disconnected:
                // remove the disconnected device from active-device map
                    String deviceId = device.id.toString();
                    if (connectingPool.containsKey(deviceId)) {
                        connectingPool[deviceId]!.cancel();
                        connectingPool.remove(deviceId);
                    }
                    if (activeDevices.containsKey(deviceId) && activeDevices[deviceId] != null) {
                        if (activeDevices[deviceId]!.onDataNotifiedSubscription != null) {
                            activeDevices[deviceId]!.onDataNotifiedSubscription!.cancel();
                        }
                        if (activeDevices[deviceId]!.onStateChangedSubscription != null) {
                            activeDevices[deviceId]!.onStateChangedSubscription!.cancel();
                        }
                    }
                    activeDevices.remove(device.id.toString());
                    if (onStateChangedCallback != null) onStateChangedCallback!(
                        device, BluetoothDeviceState.disconnected, flag: getBtStateFlag(device.id.id, BluetoothDeviceState.disconnected));
                    break;
                default:
                    if (onStateChangedCallback != null) onStateChangedCallback!(device, state, flag: getBtStateFlag(device.id.id, state));
                    break;
            }
        });

        connectingPool[device.id.toString()] = stateChangeStream;
        return future;
    }

    Future<void> _connectByName(String name, {Duration? timeout, Duration? scanTimeout, String? flag}) {
        nameToConnect = name;
        scan(timeout: scanTimeout).listen((scanResult) {
            if (scanResult.device.name == nameToConnect) {
                stopScan();
                connect(device: scanResult.device, timeout: timeout, flag: flag);
                // reset
                nameToConnect = null;
            }
        }).onError((error) {

        });
        return Future.value();
    }

    Future<void> requestMtu(int mtu, {BluetoothDevice ?device, String ?identifier}) {
        if (device != null) {
            return device.requestMtu(mtu);
        }
        if (identifier != null) {
            if (!activeDevices.containsKey(identifier)) {
                throw Exception('Unknown identifier [$identifier] to request mtu!');
            }
            return activeDevices[identifier]!.device.requestMtu(mtu);
        }
        throw Exception('Unknown device to request mtu!');
    }

    Future disconnect({BluetoothDevice ?device, String ?identifier, String? flag}) {
        if (device != null) {
            handleBtStateFlagIfNeed(device.id.id, BluetoothDeviceState.disconnected, flag);
            return device.disconnect();
        }
        if (identifier != null && activeDevices[identifier] != null) {
            handleBtStateFlagIfNeed(identifier, BluetoothDeviceState.disconnected, flag);
            return activeDevices[identifier]!.device.disconnect();
        }
        if (activeDevices.values.isEmpty) return Future.error('There is no activated device.');
        handleBtStateFlagIfNeed(activeDevices.values.first.device.id.id, BluetoothDeviceState.disconnected, flag);
        return activeDevices.values.first.device.disconnect();
    }

    void handleBtStateFlagIfNeed(String identifier, BluetoothDeviceState state, String? flag) {
        if (!btStateFlagMap.containsKey(identifier)) {
            if (flag == null) return;
            btStateFlagMap[identifier] = {};
        }
        if (flag == null) {
            btStateFlagMap[identifier]!.remove(state);
        } else {
            btStateFlagMap[identifier]![state] = flag;
        }
    }

    String? getBtStateFlag(String identifier, BluetoothDeviceState state, {bool removeAfterGet = true}) {
        if (!btStateFlagMap.containsKey(identifier)) return null;
        String? flag = btStateFlagMap[identifier]![state];
        if (flag != null && removeAfterGet) {
            btStateFlagMap[identifier]!.remove(state);
        }
        return flag;
    }

    Future write(List<int> values, {String ?identifier}) async {
        if (identifier != null && activeDevices[identifier] != null) {
            return _write0(activeDevices[identifier]!.writeCharacteristic, values, activeDevices[identifier]!.mtu);
        }
        if (activeDevices.values.isNotEmpty) {
            return _write0(activeDevices.values.first.writeCharacteristic, values, activeDevices.values.first.mtu);
        }
        print("ERROR! An error occurred when write BLE data. [There is no active device to write.]");
    }

    Future _write0(BluetoothCharacteristic characteristic, List<int> values, int sizePerPkg) async {
        StringBuffer hex = StringBuffer();
        for (int i in values)
            hex.write(i < 16 ? '0' + i.toRadixString(16) : i.toRadixString(16));
        print("write characteristic, values: " + hex.toString());
        int total = values.length,
            sentSize = 0,
            remain = total;
        if (total <= sizePerPkg) return _writePkgByPkg(characteristic, values);
        List<int> buffer;
        while (remain > 0) {
            buffer = values.sublist(sentSize, sentSize + (remain > sizePerPkg ? sizePerPkg : remain));
            await _writePkgByPkg(characteristic, buffer);
            sentSize += buffer.length;
            remain = total - sentSize;
        }
    }

    Future _writePkgByPkg(BluetoothCharacteristic characteristic, List<int> values) {
        return characteristic.write(values);
    }

    ActiveDevice? retrieveActiveDevice(BluetoothDevice device, List<BluetoothService> services) {
        BluetoothService ?writeService;
        BluetoothCharacteristic ?writeCharacteristic;
        BluetoothService ?notifyService;
        BluetoothCharacteristic ?notifyCharacteristic;
        for (BleUuid supportedUuid in supportedUuidList) {
            for (BluetoothService service in services) {
                List<BluetoothCharacteristic> characteristics = service.characteristics;
                if (service.uuid.toString() == supportedUuid.notifyServiceUuid) {
                    for (BluetoothCharacteristic characteristic in characteristics) {
                        if (characteristic.uuid.toString() != supportedUuid.notifyCharacteristicUuid) continue;
                        notifyService = service;
                        notifyCharacteristic = characteristic;
                        break;
                    }
                    for (BluetoothCharacteristic characteristic in characteristics) {
                        if (characteristic.uuid.toString() != supportedUuid.writeCharacteristicUuid) continue;
                        writeService = service;
                        writeCharacteristic = characteristic;
                        break;
                    }
                } else if (service.uuid.toString() == supportedUuid.writeServiceUuid) {
                    for (BluetoothCharacteristic characteristic in characteristics) {
                        if (characteristic.uuid.toString() != supportedUuid.writeCharacteristicUuid) continue;
                        writeService = service;
                        writeCharacteristic = characteristic;
                        break;
                    }
                    for (BluetoothCharacteristic characteristic in characteristics) {
                        if (characteristic.uuid.toString() != supportedUuid.notifyCharacteristicUuid) continue;
                        notifyService = service;
                        notifyCharacteristic = characteristic;
                        break;
                    }
                }
            }
        }
        if (writeService == null || notifyService == null) {
            device.disconnect();
            // TODO
            return null;
        }
        notifyCharacteristic!.setNotifyValue(true);
        return ActiveDevice(device, writeService, writeCharacteristic!, notifyService, notifyCharacteristic);
    }
}

class ActiveDevice {
    BluetoothDevice device;
    BluetoothService writeService;
    BluetoothCharacteristic writeCharacteristic;
    BluetoothService notifyService;
    BluetoothCharacteristic notifyCharacteristic;

    BluetoothDeviceState state = BluetoothDeviceState.disconnected;
    Stream<BluetoothDeviceState>? stream;

    StreamSubscription ?onStateChangedSubscription;
    StreamSubscription ?onDataNotifiedSubscription;

    int mtu = 200;

    ActiveDevice(this.device, this.writeService, this.writeCharacteristic, this.notifyService, this.notifyCharacteristic);
}

class Util {
    static bool isValidMac(String ?mac) {
        return true;
    }
}

class BleUuid {

    static const String DEVICE_INFO_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb";
    static const String DEVICE_INFO_CHARACTERISTIC_UUID = "00002a23-0000-1000-8000-00805f9b34fb";

    String writeServiceUuid;

    String writeCharacteristicUuid;

    String notifyServiceUuid;

    String notifyCharacteristicUuid;

    BleUuid(this.writeServiceUuid, this.writeCharacteristicUuid, this.notifyServiceUuid, this.notifyCharacteristicUuid);
}